﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SmartMathLibrary.DataMining.ClusterAnalysis
{
    /// <summary>
    /// This class provides a KMeans cluster analysis.
    /// </summary>
    [Serializable]
    public class KMeansClusterAnalyser : AbstractClusterAnalyser
    {
        /// <summary>
        /// This field holds the clusters for the cluster analysis. 
        /// </summary>
        private readonly Cluster[] clusters;

        /// <summary>
        /// The needed iterations until the algorithm convergence.
        /// </summary>
        private int neededIterations;

        /// <summary>
        /// This field holds the data for the cluster analysis. 
        /// </summary>
        private ClusterAnalysisItem[] analysisData;

        /// <summary>
        /// This field holds the distance measure mode.
        /// </summary>
        private DistanceCalculationMode distanceMeasureMode;

        /// <summary>
        /// Initializes a new instance of the <see cref="KMeansClusterAnalyser"/> class.
        /// </summary>
        /// <param name="analysisData">The data for the cluster analysis.</param>
        /// <param name="clusters">The clusters for the cluster analysis.</param>
        public KMeansClusterAnalyser(ClusterAnalysisItem[] analysisData, Cluster[] clusters)
        {
            this.clusters = clusters;
            this.analysisData = analysisData;
            this.distanceMeasureMode = DistanceCalculationMode.Euclidean;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="KMeansClusterAnalyser"/> class.
        /// </summary>
        /// <param name="analysisData">The data for the cluster analysis.</param>
        /// <param name="clusters">The clusters for the cluster analysis.</param>
        /// <param name="distanceMode">The distance measure mode to use during the cluster analysis.</param>
        public KMeansClusterAnalyser(ClusterAnalysisItem[] analysisData, Cluster[] clusters,
                                     DistanceCalculationMode distanceMode)
        {
            this.clusters = clusters;
            this.analysisData = analysisData;
            this.distanceMeasureMode = distanceMode;
        }

        /// <summary>
        /// Gets the clusters for the cluster analysis.
        /// </summary>
        /// <value>The clusters for the cluster analysis.</value>
        public Cluster[] Clusters
        {
            get { return clusters; }
        }

        /// <summary>
        /// Gets or sets the data for the cluster analysis.
        /// </summary>
        /// <value>The data for the cluster analysis.</value>
        public ClusterAnalysisItem[] AnalysisData
        {
            get { return analysisData; }
            set { analysisData = value; }
        }

        /// <summary>
        /// Gets or sets the distance measure mode to use.
        /// </summary>
        /// <value>The distance measure mode to use.</value>
        public DistanceCalculationMode DistanceMeasureMode
        {
            get { return distanceMeasureMode; }
            set { distanceMeasureMode = value; }
        }

        /// <summary>
        /// Gets the needed iterations of the algorithm.
        /// </summary>
        /// <value>The needed iterations of the algorithm.</value>
        public int NeededIterations
        {
            get { return neededIterations; }
        }

        /// <summary>
        /// Checks the cluster positions.
        /// </summary>
        /// <param name="precision">The precision.</param>
        /// <returns>True if the cluster positions are even otherwise, false.</returns>
        private bool CheckClusterPositions(double precision)
        {
            for (int i = 0; i < this.clusters.Length; i++)
            {
                if (!this.clusters[i].NewCentroidEqualsOldCentroid(precision))
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Executes the cluster analysis.
        /// </summary>
        public override void ExecuteClusterAnalysis()
        {
            this.ExecuteClusterAnalysis(1e-5);
        }

        /// <summary>
        /// Executes the cluster analysis.
        /// </summary>
        /// <param name="precision">The precision of the cluster analysis.</param>
        public override void ExecuteClusterAnalysis(double precision)
        {
            if (this.clusters.Length < 2)
            {
                throw new ArgumentException("There have to be a minimum of two clusters.");
            }

            if (this.analysisData.Length < this.clusters.Length)
            {
                throw new ArgumentException("There have to be more data to analyse than clusters.");
            }

            bool run = true;

            this.neededIterations = 0;

            while (run)
            {
                for (int i = 0; i < this.clusters.Length; i++)
                {
                    this.clusters[i].ClusterMembers.Clear();
                }

                for (int i = 0; i < this.analysisData.Length; i++)
                {
                    int clusterNumber = 0;
                    double distance = Double.MaxValue;

                    for (int j = 0; j < this.clusters.Length; j++)
                    {
                        double tempuri = this.clusters[j].CalculateDistance(this.analysisData[i],
                                                                            this.distanceMeasureMode);

                        if (tempuri < distance)
                        {
                            distance = tempuri;
                            clusterNumber = j;
                        }
                    }

                    this.analysisData[i].ClusterIndex = clusterNumber;
                    this.clusters[clusterNumber].ClusterMembers.Add(this.analysisData[i]);
                }

                for (int i = 0; i < this.clusters.Length; i++)
                {
                    this.clusters[i].CalculateNewCentroid();
                }

                run = !this.CheckClusterPositions(precision);
                this.neededIterations++;
            }
        }
    }
}